﻿Public Class File
  Private _sections As New Dictionary(Of String, Section)(New StringComparerCaseInsensitive)

  Public Sub New()
  End Sub

  ''' <summary>
  ''' Load settings from this file.
  ''' </summary>
  Public Sub New(ByVal filename As String)
    Me.New()
    Me.LoadFile(filename)
  End Sub

  ''' <summary>
  ''' Load settings from this stream.
  ''' </summary>
  Public Sub New(ByVal data As IO.Stream)
    Me.New()
    Me.LoadData(data)
  End Sub

  ''' <summary>
  ''' Load data from this string.
  ''' </summary>
  Public Sub LoadData(ByVal data As String)
    Dim str As New IO.StringReader(data)
    Me.LoadData(str)
  End Sub

  ''' <summary>
  ''' Load settings from this stream.
  ''' </summary>
  Public Sub LoadData(ByVal data As IO.Stream)
    Dim sr As New IO.StreamReader(data, True)
    Me.LoadData(sr)
  End Sub

  ''' <summary>
  ''' Load settings from this text reader.
  ''' </summary>
  Public Sub LoadData(ByVal tr As IO.TextReader)
    Dim curSection As Section = Nothing

    While tr.Peek > -1
      Dim line As String = tr.ReadLine.Trim
      If line.Length > 0 AndAlso line(0) <> ";"c Then
        If line(0) = "["c Then
          ' find the closing bracket
          Dim cl As Integer = line.IndexOf("]"c)
          If cl > 2 Then ' Minimum section title is [X]
            curSection = New Section() ' create a new section!
            curSection.SetName(line.Substring(1, cl - 1).Trim) ' Assign the name
            If Me._sections.ContainsKey(curSection.Name) Then ' remove existing section if name is identical (can't have two keys that are the same)
              Me._sections.Remove(curSection.Name)
            End If
            Me._sections.Add(curSection.Name, curSection) ' Add the section to the list
          End If
        ElseIf curSection IsNot Nothing Then
          Dim spl As Integer = line.IndexOf("=")
          If spl > 0 AndAlso spl < line.Length - 1 Then
            Dim key As String = line.Substring(0, spl).Trim
            Dim val As String = line.Substring(spl + 1).Trim
            curSection.Add(key, val)
          ElseIf spl = line.Length - 1 Then
            Dim key As String = line.Substring(0, spl).Trim
            curSection.Add(key, "")
          End If
        End If
      End If
    End While
  End Sub

  ''' <summary>
  ''' Load settings from this file.
  ''' </summary>
  Public Sub LoadFile(ByVal filename As String)
    If IO.File.Exists(filename) Then
      Dim f As New IO.FileStream(filename, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read)
      Me.LoadData(f)
      f.Close()
    End If
  End Sub

  ''' <summary>
  ''' Saves the entire INI file to the filename specified.  Overwrites an existing file and optionally preserves comments
  ''' that may already exist in the file.
  ''' </summary>
  ''' <param name="filename">The filename to write to</param>
  ''' <param name="preserveComments">If enabled, existing comments are preserved.  Comments from sections/settings that
  ''' are deleted will be left dangling, in place.</param>
  ''' <remarks></remarks>
  Public Sub Save(ByVal filename As String, Optional ByVal preserveComments As Boolean = False)
    If preserveComments Then
      Dim newFilename As String = GetRandomNewFilename(filename)
      Dim inFile As New IO.FileStream(filename, IO.FileMode.OpenOrCreate, IO.FileAccess.Read, IO.FileShare.None)
      Dim outFile As New IO.FileStream(newFilename, IO.FileMode.Create, IO.FileAccess.Write, IO.FileShare.None)
      Dim sr As New IO.StreamReader(inFile, True)
      Dim sw As New IO.StreamWriter(outFile, System.Text.Encoding.UTF8)
      Dim cleaned As Boolean = False
      Dim leftOverList As New List(Of String)

      Try
        Dim curSection As String = ""
        Dim previousLineIsBlank As Boolean = False
        Dim missingSection As Boolean = False

        For Each s As Section In Me._sections.Values
          leftOverList.Add(s.Name.ToLower)
          For Each k As String In s.Keys
            s.TriStateValue(k) = False
          Next
        Next

        While sr.Peek > -1
          Dim line As String = sr.ReadLine
          Dim linetrim As String = line.Trim

          If linetrim.Length = 0 Then
            If previousLineIsBlank Then
              sw.WriteLine()
            End If
            previousLineIsBlank = True
          ElseIf linetrim(0) = ";" Then
            If Not missingSection Then
              If previousLineIsBlank Then
                sw.WriteLine()
                previousLineIsBlank = False
              End If
              sw.WriteLine(line)
            End If
          Else
            If linetrim(0) = "["c Then
              ' previous section
              Dim preSec As String = curSection

              ' find the closing bracket
              Dim cl As Integer = linetrim.IndexOf("]"c)
              If cl > 2 Then ' Minimum section title is [X]
                ' Assign the name
                curSection = line.Substring(1, cl - 1).Trim

                ' Remove the section from the "left over" list
                leftOverList.Remove(curSection.ToLower)

                ' We have a section.  Did we have a previous section?
                If preSec.Length > 0 Then
                  ' Write out any additional parameters NOT contained in the original file
                  If Me._sections.ContainsKey(preSec) Then
                    Dim sec As Section = Me._sections(preSec)
                    For Each k As String In sec.Keys
                      Dim v As String = sec.StringValue(k)
                      If v IsNot Nothing AndAlso Not sec.TriStateValue(k) Then
                        sw.WriteLine(k & " = " & v)
                        sec.TriStateValue(k) = True
                      End If
                    Next
                  End If
                End If

                ' Check to see if the section exists in the current INI definition... If not, don't write it out!
                If Me._sections.ContainsKey(curSection) Then
                  missingSection = False

                  ' If we have a blank line, do it here
                  If previousLineIsBlank Then
                    sw.WriteLine()
                    previousLineIsBlank = False
                  End If

                  ' Write out the new section title in the output file
                  sw.WriteLine(line)
                Else
                  missingSection = True
                End If
              End If
            ElseIf Me._sections.ContainsKey(curSection) Then
              missingSection = False

              If previousLineIsBlank Then
                sw.WriteLine()
                previousLineIsBlank = False
              End If

              Dim spl As Integer = line.IndexOf("=")
              If spl > 0 AndAlso spl < line.Length - 1 Then
                Dim key As String = line.Substring(0, spl - 1).Trim

                ' Find the key!
                If Me._sections.ContainsKey(curSection) Then
                  Dim sec As Section = Me._sections(curSection)
                  Dim nv As String = sec.StringValue(key)
                  If nv IsNot Nothing Then
                    ' Write the new value and flag the tristate
                    sw.WriteLine(key & " = " & nv)
                    sec.TriStateValue(key) = True
                  End If
                End If
              End If
            End If
          End If
        End While

        ' Output the remainer of the current section
        If Me._sections.ContainsKey(curSection) Then
          Dim cursec As Section = Me._sections(curSection)
          If previousLineIsBlank Then
            sw.WriteLine()
            previousLineIsBlank = False
          End If

          For Each k As String In cursec.Keys
            Dim v As String = cursec.StringValue(k)
            If v IsNot Nothing AndAlso Not cursec.TriStateValue(k) Then
              sw.WriteLine(k & " = " & v)
              cursec.TriStateValue(k) = True
            End If
          Next
        End If

        ' Now output all the NEW sections that were not covered above
        For Each s As String In leftOverList
          If Me._sections.ContainsKey(s) Then
            Dim sec As Section = Me._sections(s)
            sw.WriteLine()
            sw.WriteLine("[" & sec.Name & "]")
            For Each k As String In sec.Keys
              Dim v As String = sec.StringValue(k)
              If v IsNot Nothing Then
                sw.WriteLine(k & " = " & v)
              End If
            Next
          End If
        Next

        ' Clean up
        sw.Flush()
        inFile.Close()
        outFile.Close()
        cleaned = True

        ' Now move the new file over the old file
        IO.File.Delete(filename)
        IO.File.Move(newFilename, filename)
      Catch
        Try : If IO.File.Exists(newFilename) Then : IO.File.Delete(newFilename) : End If : Catch : End Try
        Throw
      Finally
        If Not cleaned Then
          Try : inFile.Close() : Catch : End Try
          Try : outFile.Close() : Catch : End Try
        End If
      End Try
    Else
      Dim f As New IO.FileStream(filename, IO.FileMode.Create, IO.FileAccess.Write, IO.FileShare.None)
      Try
        Me.Save(f)
      Finally
        Try : f.Flush() : f.Close() : Catch : End Try
      End Try
    End If
  End Sub

  ''' <summary>
  ''' Save the entire INI file (does not preserve comments if the file already exists).  Also does not seek to the beginning
  ''' of the stream and does not close the underlying stream.
  ''' </summary>
  ''' <param name="data">The stream to write to.</param>
  Public Sub Save(ByVal data As IO.Stream)
    Dim sw As New IO.StreamWriter(data, System.Text.Encoding.UTF8)
    sw.Write(Me.GetINIString)
    sw.Flush()
  End Sub

  ''' <summary>
  ''' Gets a string representation of the entire INI file.
  ''' </summary>
  Public Function GetINIString() As String
    Dim sb As New Text.StringBuilder(512)

    For Each sec As Collections.Generic.KeyValuePair(Of String, Section) In Me._sections
      sb.AppendLine("[" & sec.Key & "]")
      For Each key As String In sec.Value.Keys
        sb.AppendLine(key & " = " & sec.Value.StringValue(key))
      Next
      sb.AppendLine()
    Next

    Return sb.ToString
  End Function

  ''' <summary>
  ''' Clear all stored INI data
  ''' </summary>
  Public Sub Clear()
    For Each sec As Collections.Generic.KeyValuePair(Of String, Section) In Me._sections
      sec.Value.Clear()
    Next
    Me._sections.Clear()
  End Sub

  ''' <summary>
  ''' Get a section by name
  ''' </summary>
  ''' <param name="sectionName">Name of the section</param>
  Public ReadOnly Property Section(ByVal sectionName As String) As Section
    Get
      If Me._sections.ContainsKey(sectionName) Then
        Return Me._sections(sectionName)
      Else
        Return Nothing
      End If
    End Get
  End Property

  ''' <summary>
  ''' Get a section by name, but does not return null if it doesn't exist; returns an empty section if it would be null
  ''' </summary>
  ''' <param name="sectionName">Name of the section</param>
  Public ReadOnly Property SectionNoNull(ByVal sectionName As String) As Section
    Get
      If Me._sections.ContainsKey(sectionName) Then
        Dim sec As Section = Me._sections(sectionName)
        If sec IsNot Nothing Then
          Return sec
        Else
          Return New Section
        End If
      Else
        Return New Section
      End If
    End Get
  End Property

  ''' <summary>
  ''' Get all sections in a key/value collection
  ''' </summary>
  Public ReadOnly Property Sections() As Dictionary(Of String, Section).ValueCollection
    Get
      Return Me._sections.Values
    End Get
  End Property

  ''' <summary>
  ''' Add a new section to the INI file; returns TRUE on success (false means the section already exists)
  ''' </summary>
  Public Function AddSection(ByVal sectionName As String) As Boolean
    If Me._sections.ContainsKey(sectionName) Then
      Return False
    Else
      Dim sec As New Section
      sec.SetName(sectionName)
      Me._sections.Add(sectionName, sec)
      Return True
    End If
  End Function

  ''' <summary>
  ''' Remove a section from the INI file; returns TRUE on success (false means the section does not exist)
  ''' </summary>
  Public Function RemoveSection(ByVal sectionName As String) As Boolean
    If Me._sections.ContainsKey(sectionName) Then
      Me._sections.Remove(sectionName)
      Return True
    Else
      Return False
    End If
  End Function

  ''' <summary>
  ''' Takes a filename, then generates a new one with a new extension that is random and does not have a collision.
  ''' </summary>
  Private Shared Function GetRandomNewFilename(ByVal name As String) As String
    Dim num As Integer = 1
    Dim path As String

    Do
      path = name & "." & num.ToString
      num += 1
    Loop While IO.File.Exists(path)

    Return path
  End Function

End Class
